package com.jichangxiu.framework.aspect;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.jichangxiu.common.annotation.Delete;
import com.jichangxiu.common.annotation.Get;
import com.jichangxiu.common.annotation.Post;
import com.jichangxiu.common.annotation.Put;
import com.jichangxiu.common.cache.RedisService;
import com.jichangxiu.common.constant.Constant;
import com.jichangxiu.common.constant.LimitType;
import com.jichangxiu.common.constant.RedisKeys;
import com.jichangxiu.common.entity.bo.Record;
import com.jichangxiu.common.entity.bo.Result;
import com.jichangxiu.common.exception.FrameworkException;
import com.jichangxiu.common.exception.ServiceException;
import com.jichangxiu.common.utils.*;
import com.jichangxiu.framework.factory.AsyncFactory;
import com.jichangxiu.framework.service.LogService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;

/**
 * RequestAspect:
 *
 * @author Jcx
 * @create 2023-04-04 10:27
 */
@Slf4j
@Aspect
@Component
public class RequestAspect {

    @Resource
    private TokenUtils tokenUtils;

    @Resource
    private LockUtils lockUtils;

    @Resource
    private RedisService redisService;

    @Resource
    private RedisScript<Long> limitScript;

    private LogService logService;

    @Autowired
    public void setLogService(LogService logService) {
        this.logService = logService;
    }

    @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)" + "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)"
            + "|| @annotation(org.springframework.web.bind.annotation.PutMapping)" + "|| @annotation(org.springframework.web.bind.annotation.GetMapping)"
            + "|| @annotation(org.springframework.web.bind.annotation.RequestMapping)" + "|| @annotation(org.springframework.web.bind.annotation.RestController)")
    public void pointcutAround() {
    }

    @Around("pointcutAround()")
    public Object run(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.nanoTime();
        Object object;
        try {
            long start = System.nanoTime();
            object = joinPoint.proceed();
            log.warn("非自定义注解请求 " + ServletUtils.getRequest().getRequestURI() + " 业务处理耗时：" + (System.nanoTime() - start) / 1000000.0);
        } catch (Exception ex) {
            if (ex instanceof ServiceException) {
                throw ex;
            } else {
                log.error(ex.getMessage(), ex);
                throw FrameworkException.build(ex.getMessage(), ex);
            }
        }
        log.warn("非自定义注解请求 " + ServletUtils.getRequest().getRequestURI() + " AOP处理耗时：" + (System.nanoTime() - startTime) / 1000000.0);
        return object;
    }

    @Pointcut("@annotation(post)")
    public void jcxPostMappingPointcutAround(Post post) {
    }

    @Around(value = "jcxPostMappingPointcutAround(post)", argNames = "joinPoint,post")
    public Object jcxPostMappingAround(ProceedingJoinPoint joinPoint, Post post) throws Throwable {
        long startTime = System.nanoTime();
        if (post.unifyResponseEnabled()) {
            log.warn("请求 " + ServletUtils.getRequest().getRequestURI() + " 开启了统一返回值封装");
            ServletUtils.getRequest().setAttribute(Constant.CUSTOM_MAPPING_ADD_ATTRIBUTE, Constant.CUSTOM_MAPPING_ADD_ATTRIBUTE);
        }
        if (post.parameterTrimEnabled()) beanTrim(joinPoint);
        if (post.rateLimiterEnabled()) rateLimiter(joinPoint, post.limitTime(), post.limitCount(), post.limitType());
        Object body = doAround(joinPoint, post.parameterRepeatEnabled(), post.repeatTime(), post.logEnabled());
        log.warn("请求 " + ServletUtils.getRequest().getRequestURI() + " AOP处理耗时：" + (System.nanoTime() - startTime) / 1000000.0);
        return body;
    }

    @Pointcut("@annotation(delete)")
    public void jcxDeleteMappingPointcutAround(Delete delete) {
    }

    @Around(value = "jcxDeleteMappingPointcutAround(delete)", argNames = "joinPoint,delete")
    public Object jcxDeleteMappingAround(ProceedingJoinPoint joinPoint, Delete delete) throws Throwable {
        long startTime = System.nanoTime();
        if (delete.unifyResponseEnabled()) {
            log.warn("请求 " + ServletUtils.getRequest().getRequestURI() + " 开启了统一返回值封装");
            ServletUtils.getRequest().setAttribute(Constant.CUSTOM_MAPPING_ADD_ATTRIBUTE, Constant.CUSTOM_MAPPING_ADD_ATTRIBUTE);
        }
        if (delete.parameterTrimEnabled()) beanTrim(joinPoint);
        if (delete.rateLimiterEnabled())
            rateLimiter(joinPoint, delete.limitTime(), delete.limitCount(), delete.limitType());
        Object body = doAround(joinPoint, delete.parameterRepeatEnabled(), delete.repeatTime(), delete.logEnabled());
        log.warn("请求 " + ServletUtils.getRequest().getRequestURI() + " AOP处理耗时：" + (System.nanoTime() - startTime) / 1000000.0);
        return body;
    }

    @Pointcut("@annotation(put)")
    public void jcxPutMappingPointcutAround(Put put) {
    }

    @Around(value = "jcxPutMappingPointcutAround(put)", argNames = "joinPoint,put")
    public Object jcxPutMappingAround(ProceedingJoinPoint joinPoint, Put put) throws Throwable {
        long startTime = System.nanoTime();
        if (put.unifyResponseEnabled()) {
            log.warn("请求 " + ServletUtils.getRequest().getRequestURI() + " 开启了统一返回值封装");
            ServletUtils.getRequest().setAttribute(Constant.CUSTOM_MAPPING_ADD_ATTRIBUTE, Constant.CUSTOM_MAPPING_ADD_ATTRIBUTE);
        }
        if (put.parameterTrimEnabled()) beanTrim(joinPoint);
        if (put.rateLimiterEnabled()) rateLimiter(joinPoint, put.limitTime(), put.limitCount(), put.limitType());
        Object body = doAround(joinPoint, put.parameterRepeatEnabled(), put.repeatTime(), put.logEnabled());
        log.warn("请求 " + ServletUtils.getRequest().getRequestURI() + " AOP处理耗时：" + (System.nanoTime() - startTime) / 1000000.0);
        return body;
    }

    @Pointcut("@annotation(get)")
    public void jcxGetMappingPointcutAround(Get get) {
    }

    @Around(value = "jcxGetMappingPointcutAround(get)", argNames = "joinPoint,get")
    public Object jcxGetMappingAround(ProceedingJoinPoint joinPoint, Get get) throws Throwable {
        long startTime = System.nanoTime();
        if (get.unifyResponseEnabled()) {
            log.warn("请求 " + ServletUtils.getRequest().getRequestURI() + " 开启了统一返回值封装");
            ServletUtils.getRequest().setAttribute(Constant.CUSTOM_MAPPING_ADD_ATTRIBUTE, Constant.CUSTOM_MAPPING_ADD_ATTRIBUTE);
        }
        if (get.parameterTrimEnabled()) beanTrim(joinPoint);
        if (get.rateLimiterEnabled()) rateLimiter(joinPoint, get.limitTime(), get.limitCount(), get.limitType());
        Object body = doAround(joinPoint, get.parameterRepeatEnabled(), get.repeatTime(), get.logEnabled());
        log.warn("请求 " + ServletUtils.getRequest().getRequestURI() + " AOP处理耗时：" + (System.nanoTime() - startTime) / 1000000.0);
        return body;
    }

    private Object doAround(ProceedingJoinPoint joinPoint, boolean parameterRepeatEnabled, int lockTime, boolean logEnabled) throws Throwable {
        Record logRecord = null;
        if (logEnabled) {
            logRecord = getLogRecord(joinPoint);
        }

        if (parameterRepeatEnabled) {
            log.warn("请求 " + ServletUtils.getRequest().getRequestURI() + " 开启了防重复请求");
            String jti = tokenUtils.getJti();
            if (StrUtil.isNotEmpty(jti)) {
                String clientId = IdUtil.randomUUID();
                String redisKey = RedisKeys.createCodeToRequestRepeat(ServletUtils.getPath() + ":" + jti);
                boolean lock = lockUtils.lock(redisKey, clientId, lockTime);
                if (lock) {
                    Object object;
                    try {
                        long start = System.nanoTime();
                        object = joinPoint.proceed();
                        log.warn("请求 " + ServletUtils.getRequest().getRequestURI() + " 业务处理耗时：" + (System.nanoTime() - start) / 1000000.0);
                        if (logEnabled) {
                            logRecord.put("result", Result.toResult(object));
                            logRecord.put("status", true);
                        }
                    } catch (Exception ex) {
                        if (logEnabled) {
                            logRecord.put("exception", ex.getMessage());
                            logRecord.put("status", false);
                        }
                        throw ServiceException.build(ex.getMessage());
                    } finally {
                        lockUtils.unlock(redisKey, clientId);
                        if (logEnabled) {
                            // 保存数据库
                            AsyncUtils.getINSTANCE().execute(AsyncFactory.recordPlatformLog(logService, logRecord));
                        }
                    }
                    return object;
                } else {
                    if (logEnabled) {
                        logRecord.put("exception", "操作过快");
                        logRecord.put("status", false);
                        AsyncUtils.getINSTANCE().execute(AsyncFactory.recordPlatformLog(logService, logRecord));
                    }
                    throw ServiceException.build("操作过快");
                }
            }
        }

        Object object;
        try {
            long start = System.nanoTime();
            object = joinPoint.proceed();
            log.warn("请求 " + ServletUtils.getRequest().getRequestURI() + " 业务处理耗时：" + (System.nanoTime() - start) / 1000000.0);
            if (logEnabled) {
                logRecord.put("result", Result.toResult(object));
                logRecord.put("status", true);
            }
        } catch (Exception ex) {
            if (logEnabled) {
                logRecord.put("exception", ex.getMessage());
                logRecord.put("status", false);
            }
            if (ex instanceof ServiceException) {
                throw ex;
            } else {
                log.error(ex.getMessage(), ex);
                throw FrameworkException.build(ex.getMessage(), ex);
            }
        } finally {
            if (logEnabled) {
                // 保存数据库
                AsyncUtils.getINSTANCE().execute(AsyncFactory.recordPlatformLog(logService, logRecord));
            }
        }
        return object;
    }

    private void beanTrim(ProceedingJoinPoint joinPoint) {
        log.warn("请求 " + ServletUtils.getRequest().getRequestURI() + "开启了参数去空");
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Post post = method.getAnnotation(Post.class);
        Delete delete = method.getAnnotation(Delete.class);
        Put put = method.getAnnotation(Put.class);
        if (ObjectUtil.isNotEmpty(post) || ObjectUtil.isNotEmpty(delete) || ObjectUtil.isNotEmpty(put)) {
            Object[] args = joinPoint.getArgs();
            for (Object arg : args) {
                try {
                    StrUtils.trimBeanStr(arg);
                } catch (Exception e) {
                    throw new RuntimeException("参数处理异常");
                }
            }
        }
    }

    private void rateLimiter(ProceedingJoinPoint joinPoint, int limitTime, int limitCount, LimitType limitType) {
        log.warn("请求 " + ServletUtils.getRequest().getRequestURI() + "开启了请求访问限流");
        String limitKey = getLimitKey(joinPoint, limitType);
        List<Object> keys = Collections.singletonList(limitKey);
        try {
            Long number = redisService.execute(limitScript, keys, limitCount, limitTime);
            if (ObjectUtil.isNull(number) || number.intValue() > limitCount) {
                throw ServiceException.build("访问过于频繁，请稍候再试");
            }
            log.info("限制请求'{}',当前请求'{}',缓存key'{}'", limitCount, number.intValue(), limitKey);
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("服务器限流异常，请稍候再试");
        }
    }

    private String getLimitKey(ProceedingJoinPoint joinPoint, LimitType limitType) {
        StringBuilder stringBuffer = new StringBuilder();
        if (limitType.equals(LimitType.IP)) {
            stringBuffer.append(RedisKeys.createCodeToRateLimit(IpUtils.getIpAddr(ServletUtils.getRequest()))).append("-");
        }
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
        return stringBuffer.toString();
    }

    private Record getLogRecord(ProceedingJoinPoint joinPoint) {
        // 设置方法名称
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        String url = ServletUtils.getRequest().getRequestURI();
        String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
        String method = ServletUtils.getRequest().getMethod();

        Record record = Record.getRecord();
        if (HttpMethod.POST.matches(method)) {
            record.put("serviceType", "增加");
            record.put("operatorType", "ADD");
        } else if (HttpMethod.DELETE.matches(method)) {
            record.put("serviceType", "删除");
            record.put("operatorType", "DELETE");
        } else if (HttpMethod.PUT.matches(method)) {
            record.put("serviceType", "修改");
            record.put("operatorType", "EDIT");
        } else if (HttpMethod.GET.matches(method)) {
            record.put("serviceType", "查询");
            record.put("operatorType", "QUERY");
        } else {
            record.put("serviceType", "其他");
            record.put("operatorType", "OTHER");
        }

        record.put("httpMethod", method);
        record.put("model", className);
        record.put("method", methodName);
        record.put("url", url);
        record.put("ip", ip);
        record.put("param", Record.getRequestRecord(ServletUtils.getRequest()));

        try {
            record.put("userId", tokenUtils.getSecurityUser().getUserId());
            record.put("tenantId", tokenUtils.getTenantId());
        } catch (Exception exception) {
            record.put("userId", "未登录用户");
            record.put("tenantId", "未登录用户");
        }

        return record;
    }

}
